// SPDX-License-Identifier: GPL-2.0
/*
 * EHCI HCD (Host Controller Driver) for USB.
 *
 * (C) Copyright 2010 Hisilicon
 *
 * This file is licenced under the GPL.
 */
#ifdef CONFIG_HIUSB_EHCI
#include <linux/platform_device.h>
#include <linux/dma-map-ops.h>

#include "hiusb_ont.h"
#include "ehci.h"

extern void hiusb_start_hcd(void);
extern void hiusb_stop_hcd(void);

struct resource g_ehci_res[] = {
	[0] = {
		.start = CONFIG_HIUSB_EHCI_IOBASE,
		.end   = CONFIG_HIUSB_EHCI_IOBASE + CONFIG_HIUSB_EHCI_IOSIZE - 1,
		.flags = IORESOURCE_MEM,
	},
	[1] = {
		.start = CONFIG_HIUSB_EHCI_IRQNUM,
		.end   = CONFIG_HIUSB_EHCI_IRQNUM,
		.flags = IORESOURCE_IRQ,
	},
};

u64 g_usb_dmamask_ehci = DMA_BIT_MASK(32);
struct platform_device g_ehci_platdev = {
	.name = "hiusb-ehci",
	.id   = 0,
	.dev  = {
		.platform_data		= 0,
		.dma_mask		= &g_usb_dmamask_ehci,
		.coherent_dma_mask	= DMA_BIT_MASK(32),
		.release		= 0,
	},
	.num_resources = ARRAY_SIZE(g_ehci_res),
	.resource      = g_ehci_res,
};

int hiusb_ehci_init(void)
{
	int ret = 0;

	ret = ehci_and_ohci_chip_id_check();
	if (ret != 0)
		return -ENODEV;

	ret = platform_device_register(&g_ehci_platdev);
	if (ret != 0) {
		pr_err("%s->%d, platform_device_register fail.\n", __func__, __LINE__);
		return -ENODEV;
	}

	return 0;
}

int hiusb_driver_register(struct platform_driver *driver)
{
	int ret;

	ret = hiusb_ehci_init();
	if (ret != 0)
		return -ENODEV;

	ret = platform_driver_register(driver);
	if (ret != 0)
		return -ENODEV;

	return 0;
}

void hiusb_driver_unregister(struct platform_driver *driver)
{
	platform_device_unregister(&g_ehci_platdev);

	platform_driver_unregister(driver);
}

static const struct hc_driver g_ehci_hc_driver = {
	.description	= hcd_name,
	.product_desc	= "hiusb-ehci",
	.hcd_priv_size	= sizeof(struct ehci_hcd),

	/*
	 * generic hardware linkage
	 */
	.irq	= ehci_irq,
	.flags	= HCD_MEMORY | HCD_DMA | HCD_USB2,

	/*
	 * basic lifecycle operations
	 */
	.reset		= ehci_init,
	.start		= ehci_run,
	.stop		= ehci_stop,
	.shutdown	= ehci_shutdown,

	/*
	 * managing i/o requests and associated device resources
	 */
	.urb_enqueue		= ehci_urb_enqueue,
	.urb_dequeue		= ehci_urb_dequeue,
	.endpoint_disable	= ehci_endpoint_disable,

	/*
	 * scheduling support
	 */
	.get_frame_number	= ehci_get_frame,

	/*
	 * root hub support
	 */
	.hub_status_data	= ehci_hub_status_data,
	.hub_control		= ehci_hub_control,
	.bus_suspend		= ehci_bus_suspend,
	.bus_resume		= ehci_bus_resume,
	.relinquish_port	= ehci_relinquish_port,
	.port_handed_over	= ehci_port_handed_over,
};

static int hiusb_ehci_hcd_drv_probe(struct platform_device *pdev)
{
	struct usb_hcd *hcd;
	struct ehci_hcd *ehci;
	struct resource *res;
	int ret;
	struct usb_logic_port *usb_ehci_logic_port = NULL;

	ret = hiusb_ehci_ohci_chip_id_check(g_ehci_res, usb_ehci_logic_port, pdev,
		USB_TYPE_ONT_EHCI);
	if (ret != 0)
		return ret;

	if (usb_disabled())
		return -ENODEV;

	if (pdev->resource[1].flags != IORESOURCE_IRQ) {
		pr_debug("resource[1] is not IORESOURCE_IRQ");
		return -ENOMEM;
	}

	hcd = usb_create_hcd(&g_ehci_hc_driver, &pdev->dev, "hiusb-ehci");
	if (hcd == 0)
		return -ENOMEM;

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	hcd->rsrc_start = res->start;
	hcd->rsrc_len = resource_size(res);
	if (request_mem_region(hcd->rsrc_start, hcd->rsrc_len, hcd_name) == 0) {
		pr_err("request_mem_region failed");
		usb_put_hcd(hcd);
		return -EBUSY;
	}

	hcd->regs = ioremap(hcd->rsrc_start, hcd->rsrc_len);
	if (!hcd->regs) {
		pr_err("ioremap failed");
		release_mem_region(hcd->rsrc_start, hcd->rsrc_len);
		usb_put_hcd(hcd);
		return -ENOMEM;
	}

	hiusb_start_hcd();

	ehci = hcd_to_ehci(hcd);
	ehci->caps = hcd->regs;
	ehci->regs = hcd->regs + HC_LENGTH(ehci, readl(&ehci->caps->hc_capbase));
	/* cache this readonly data; minimize chip reads */
	ehci->hcs_params = readl(&ehci->caps->hcs_params);

	ret = usb_add_hcd(hcd, pdev->resource[1].start, IRQF_SHARED);
	if (ret != 0) {
		hiusb_stop_hcd();
		iounmap(hcd->regs);
		release_mem_region(hcd->rsrc_start, hcd->rsrc_len);
		usb_put_hcd(hcd);
		return ret;
	}
	platform_set_drvdata(pdev, hcd);
	return 0;
}

static int hiusb_ehci_hcd_drv_remove(struct platform_device *pdev)
{
	struct usb_hcd *hcd = platform_get_drvdata(pdev);

	usb_remove_hcd(hcd);
	iounmap(hcd->regs);
	release_mem_region(hcd->rsrc_start, hcd->rsrc_len);
	usb_put_hcd(hcd);
	hiusb_stop_hcd();
	platform_set_drvdata(pdev, NULL);

	return 0;
}

#ifdef CONFIG_PM
static int hiusb_ehci_hcd_drv_suspend(struct device *dev)
{
	struct usb_hcd *hcd = dev_get_drvdata(dev);
	struct ehci_hcd *ehci = hcd_to_ehci(hcd);
	unsigned long flags;
	int rc = 0;

	if (time_before(jiffies, ehci->next_statechange))
		msleep(20);

	/* Root hub was already suspended. Disable irq emission and
	 * mark HW unaccessible.  The PM and USB cores make sure that
	 * the root hub is either suspended or stopped.
	 */
	spin_lock_irqsave(&ehci->lock, flags);
	ehci_prepare_ports_for_controller_suspend(ehci, device_may_wakeup(dev));
	ehci_writel(ehci, 0, &ehci->regs->intr_enable);
	(void)ehci_readl(ehci, &ehci->regs->intr_enable);

	clear_bit(HCD_FLAG_HW_ACCESSIBLE, &hcd->flags);

	hiusb_stop_hcd();
	spin_unlock_irqrestore(&ehci->lock, flags);

	// could save FLADJ in case of Vaux power loss
	// ... we'd only use it to handle clock skew

	return rc;
}

static void ehci_port_power_oxu(struct ehci_hcd *ehci, bool is_on)
{
	unsigned int port;
	u32 __iomem *status_reg;
	u32 temp;
	struct usb_hcd *hcd = ehci_to_hcd(ehci);

	if (!HCS_PPC(ehci->hcs_params))
		return;

	port = HCS_N_PORTS(ehci->hcs_params);
	status_reg = &ehci->regs->port_status[port];
	temp = ehci_readl(ehci, status_reg) & ~PORT_RWC_BITS;

	while (port > 0) {
		if (is_on)
			ehci_writel(ehci, temp | PORT_POWER, status_reg);
		else
			ehci_writel(ehci, temp & ~PORT_POWER, status_reg);

		if (hcd->driver->port_power)
			hcd->driver->port_power(hcd, port, is_on);
	}

	msleep(20);
}

static int hiusb_ehci_hcd_drv_resume(struct device *dev)
{
	struct usb_hcd *hcd = dev_get_drvdata(dev);
	struct ehci_hcd *ehci = hcd_to_ehci(hcd);

	hiusb_start_hcd();

	// maybe restore FLADJ

	if (time_before(jiffies, ehci->next_statechange))
		msleep(100);

	/* Mark hardware accessible again as we are out of D3 state by now */
	set_bit(HCD_FLAG_HW_ACCESSIBLE, &hcd->flags);

	/* If CF is still set, we maintained PCI Vaux power.
	 * Just undo the effect of ehci_pci_suspend().
	 */
	if (ehci_readl(ehci, &ehci->regs->configured_flag) == FLAG_CF) {
		int mask = INTR_MASK;

		ehci_prepare_ports_for_controller_resume(ehci);
		if (!hcd->self.root_hub->do_remote_wakeup)
			mask &= ~STS_PCD;
		ehci_writel(ehci, mask, &ehci->regs->intr_enable);
		ehci_readl(ehci, &ehci->regs->intr_enable);
		return 0;
	}

	ehci_dbg(ehci, "lost power, restarting\n");
	usb_root_hub_lost_power(hcd->self.root_hub);

	/* Else reset, to cope with power loss or flush-to-storage
	 * style "resume" having let BIOS kick in during reboot.
	 */
	(void) ehci_halt(ehci);
	(void) ehci_reset(ehci);

	/* emptying the schedule aborts any urbs */
	spin_lock_irq(&ehci->lock);
	if (ehci->async_unlinking)
		end_unlink_async(ehci);
	ehci_work(ehci);
	spin_unlock_irq(&ehci->lock);

	ehci_writel(ehci, ehci->command, &ehci->regs->command);
	ehci_writel(ehci, FLAG_CF, &ehci->regs->configured_flag);
	ehci_readl(ehci, &ehci->regs->command);    /* unblock posted writes */

	/* here we "know" root ports should always stay powered */
	ehci_port_power_oxu(ehci, true);

	hcd->state = HC_STATE_SUSPENDED;

	return 0;
}

static const struct dev_pm_ops hiusb_ehci_pmops = {
	.suspend    = hiusb_ehci_hcd_drv_suspend,
	.resume     = hiusb_ehci_hcd_drv_resume,
};

#define HIUSB_EHCI_PMOPS (&hiusb_ehci_pmops)

#else
#define HIUSB_EHCI_PMOPS NULL
#endif

struct platform_driver hiusb_ehci_hcd_driver = {
	.probe       = hiusb_ehci_hcd_drv_probe,
	.remove      = hiusb_ehci_hcd_drv_remove,
	.shutdown    = usb_hcd_platform_shutdown,
	.driver = {
		.name    = "hiusb-ehci",
		.owner   = THIS_MODULE,
		.pm      = HIUSB_EHCI_PMOPS,
	}
};

MODULE_ALIAS("platform:hi-ehci");

#endif /* CONFIG_HIUSB_EHCI */
